﻿
namespace Orchard.OData.Services {
    using Microsoft.OData.Core.UriParser.Semantic;
    using Microsoft.OData.Core.UriParser.TreeNodeKinds;
    using Microsoft.OData.Edm;
    using NHibernate;
    using NHibernate.Criterion;
    using NHibernate.OData.Extensions;
    using NHibernate.Type;
    using Orchard.ContentManagement;
    using Orchard.ContentManagement.Records;
    using System;
    using System.Collections.Generic;
    using System.Linq;

    internal sealed class NHibernateProjectionVisitor : NHibernateAbstractVisitor<IProjection> {
        private static readonly IType DefaultArithmeticReturnType = NHibernateUtil.Decimal; // Arithmetic operations on two columns default to decimal
        private readonly Dictionary<string, Func<IEnumerable<QueryNode>, IProjection>> projectionMethodVisitorFactory;

        internal NHibernateProjectionVisitor(ICriteria contentItemVersionRecordCriteria, IEdmModel edmModel)
            : base(contentItemVersionRecordCriteria, edmModel) {
            this.projectionMethodVisitorFactory = new Dictionary<string, Func<IEnumerable<QueryNode>, IProjection>>() 
            {
                {"substring", this.SubStringOfMethod},
                {"concat", this.ConcatMethod},
                {"length", this.LengthMethod},
                {"replace", this.ReplaceMethod},
                {"toupper", this.ToUpperMethod},
                {"tolower", this.ToLowerMethod},
                {"trim", this.TrimMethod},
                {"indexof", this.IndexOfMethod},
                {"ceiling", this.CeilingMethod},
                {"floor", this.FloorMethod},
                {"round", this.RoundMethod},
                {"cast", this.CastMethod},
                {"year", this.YearMethod},
                {"month", this.MonthMethod},
                {"day", this.DayMethod},
                {"minute", this.MinuteMethod},
                {"second", this.SecondMethod},
            };
        }

        public IProjection Visit(QueryNode queryNode) {
            return queryNode.Accept<IProjection>(this);
        }

        public override IProjection Visit(ConstantNode nodeIn) {
            return Projections.Constant(nodeIn.Value, this.nHibernateTypeFromLiteralType(nodeIn.TypeReference));
        }

        public override IProjection Visit(ConvertNode nodeIn) {
            var sourceNode = nodeIn.Source;
            if (!(sourceNode is ConstantNode)) {
                return null;
            }

            var constantNode = (ConstantNode)sourceNode;
            var convertType = nodeIn.TypeReference;
            object convertedValue = constantNode.Value.ToString();;
            if (convertType.IsDouble()){
                convertedValue = Convert.ToDouble(constantNode.Value);
            }
            if (convertType.IsInt16()) {
                convertedValue = Convert.ToInt16(constantNode.Value);
            }
            if (convertType.IsInt16()) {
                convertedValue = Convert.ToInt32(constantNode.Value);
            }
            if (convertType.IsInt64()) {
                convertedValue = Convert.ToInt64(constantNode.Value);
            }
            if (convertType.IsDecimal()) {
                convertedValue = Convert.ToDecimal(constantNode.Value);
            }

            var convertNode = new ConstantNode(convertedValue);
            return convertNode.Accept<IProjection>(this);
        }

        public override IProjection Visit(SingleValuePropertyAccessNode nodeIn) {
            var singleValuePropertyAccessName = nodeIn.Property.Name;
            var singleValuePropertyDeclaringType = nodeIn.Property.DeclaringType;

            var contentField = this.edmModel.GetAnnotationValue<ContentField>(singleValuePropertyDeclaringType);
            var contentPart = this.edmModel.GetAnnotationValue<ContentPart>(singleValuePropertyDeclaringType);
            var contentItem = this.edmModel.GetAnnotationValue<ContentItem>(singleValuePropertyDeclaringType);

            if (null != contentField) {
                return this.Visit(nodeIn, contentField);
            }
            if (null != contentPart) {
                return this.Visit(nodeIn, contentPart);
            }
            if (null != contentItem) {
                return this.Visit(nodeIn, contentItem);
            }

            return null;
        }

        private IProjection Visit(SingleValuePropertyAccessNode nodeIn, ContentField contentField) {
            var contentFieldType = contentField.GetType();
            
            var contentFiedProperties = contentFieldType.GetProperties();
            if (!contentFiedProperties.Any(p => p.Name == nodeIn.Property.Name)) {
                return null;
            }

            var contentFieldProperty = contentFiedProperties.First(p => p.Name == nodeIn.Property.Name);
            var literalType = contentFieldProperty.PropertyType;
            
            var contentFieldTable = string.Empty;
            if (typeof(int) == literalType || typeof(int?) == literalType) {
                contentFieldTable = "IntegerFieldIndexRecords";
            }
            if (typeof(decimal) == literalType || typeof(decimal?) == literalType) {
                contentFieldTable = "DecimalFieldIndexRecords";
            }
            if (typeof(double) == literalType || typeof(double?) == literalType) {
                contentFieldTable = "DoubleFieldIndexRecords";
            }
            if (typeof(string) == literalType) {
                contentFieldTable = "StringFieldIndexRecords";
            }
            if (string.IsNullOrEmpty(contentFieldTable)) {
                return null;
            }

            var contentTypeReference = (nodeIn.Source as SingleValuePropertyAccessNode).Source.TypeReference;
            var contentTypeDefinition = contentTypeReference.Definition;
            var contentTypeName = this.edmModel.GetAnnotationValue<ContentItem>(contentTypeDefinition).TypeDefinition.Name;

            var aliasJoinName = contentTypeName + contentFieldTable;
            var fieldIndexPartRecordCriteria = this.contentItemRecordCriteria
                .BindCriteriaByPath("FieldIndexPartRecord")
                .BindCriteriaByAlias(contentFieldTable, aliasJoinName, NHibernate.SqlCommand.JoinType.LeftOuterJoin);
            fieldIndexPartRecordCriteria.Add(Restrictions.Eq("PropertyName", contentTypeName + "." + contentField.Name + "."));

            return Projections.Property(aliasJoinName + ".Value");
        }

        private IProjection Visit(SingleValuePropertyAccessNode nodeIn, ContentPart contentPart) {
            var contentPartType = contentPart.GetType();
            var contentTypeDefinition = contentPart.TypeDefinition;

            var contentPartProperties = contentPartType.GetProperties();
            if (!contentPartProperties.Any(f => f.Name == nodeIn.Property.Name)) {
                // == Does OData enable to query fields upon parts embedded into the contentType requested ? ==
                // == May proptocol authorize several levels for property access ?
                // == ~/odata/<+contentTypes+>/$filter=<+contentPart+>/<+contentField+>/<+propertyName+> eq <+something+>
                return null;
            }

            var contentPartRecordProperty = contentPartProperties.FirstOrDefault(prop => prop.Name == "Record");
            if (null == contentPartRecordProperty) {
                return null;
            }

            var recordType = contentPartRecordProperty.PropertyType;
            if (recordType.IsSubclassOf(typeof(ContentPartVersionRecord))) {
                this.contentItemVersionRecordCriteria.BindCriteriaByAlias(recordType.Name, contentPartType.Name);
            }
            else {
                this.contentItemRecordCriteria.BindCriteriaByAlias(recordType.Name, contentPartType.Name);
            }

            return Projections.Property(contentPartType.Name + "." + nodeIn.Property.Name);            
        }

        private IProjection Visit(SingleValuePropertyAccessNode nodeIn, ContentItem contentItem) {
            var contentItemType = contentItem.GetType();
            var contentItemProperties = contentItemType.GetProperties();
            
            var contentItemProperty = contentItemProperties.FirstOrDefault(p => p.Name == nodeIn.Property.Name);
            if (null == contentItemProperty) {
                return null;
            }
            
            return Projections.Property(nodeIn.Property.Name);
        }

        public override IProjection Visit(SingleValueFunctionCallNode nodeIn) {
            var methodName = nodeIn.Name.ToLower();
            if (!this.projectionMethodVisitorFactory.ContainsKey(methodName)) {
                return null;
            }
            var method = this.projectionMethodVisitorFactory[methodName];
            return method(nodeIn.Parameters);
        }

        public override IProjection Visit(BinaryOperatorNode nodeIn) {
            var operatorKind = nodeIn.OperatorKind;
            var leftNode = nodeIn.Left;
            var rightNode = nodeIn.Right;

            if (this.IsArithmeticalOperator(operatorKind)) {
                var leftProjection = leftNode.Accept<IProjection>(this);
                var rightProjection = rightNode.Accept<IProjection>(this);
                var returnType = this.ArtithmicReturnType(leftNode, rightNode);

                if (false == (null != leftProjection && null != rightProjection)) {
                    return null;
                }

                switch (operatorKind) {
                    case BinaryOperatorKind.Add: return new ArithmeticOperatorProjection("+", returnType, leftProjection, rightProjection);
                    case BinaryOperatorKind.Subtract: return new ArithmeticOperatorProjection("-", returnType, leftProjection, rightProjection);
                    case BinaryOperatorKind.Multiply: return new ArithmeticOperatorProjection("*", returnType, leftProjection, rightProjection);
                    case BinaryOperatorKind.Divide: return new ArithmeticOperatorProjection("/", returnType, leftProjection, rightProjection);
                    case BinaryOperatorKind.Modulo: return new SqlFunctionProjection("mod", returnType, leftProjection, rightProjection);
                    default: return null;
                }
            }

            return null;
        }

        public IProjection SubStringOfMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();

            if (arguments.Count == 2) {
                return new SqlFunctionProjection(
                    "substring",
                    NHibernateUtil.String,
                    arguments[0].Accept<IProjection>(this),
                    arguments[1].Accept<IProjection>(this)
                );
            }

            return new SqlFunctionProjection(
                "substring",
                NHibernateUtil.String,
                arguments[0].Accept<IProjection>(this),
                arguments[1].Accept<IProjection>(this),
                arguments[2].Accept<IProjection>(this)
            );
        }

         public IProjection ConcatMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();

            if (arguments.Count == 1) {
                return arguments[0].Accept<IProjection>(this);
            }

            return new SqlFunctionProjection(
                "concat",
                NHibernateUtil.String,
                arguments[0].Accept<IProjection>(this),
                arguments[1].Accept<IProjection>(this)
            );
        }

        public IProjection LengthMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            
            if (1 > arguments.Count) {
                return null;
            }

            return new SqlFunctionProjection(
                "length",
                NHibernateUtil.Int32,
                arguments[0].Accept<IProjection>(this)
            );
        }

        public IProjection ReplaceMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            
            return new SqlFunctionProjection(
                "replace",
                NHibernateUtil.String,
                arguments[0].Accept<IProjection>(this),
                arguments[1].Accept<IProjection>(this),
                arguments[2].Accept<IProjection>(this)
            );
        }

        public IProjection ToUpperMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            
            return new SqlFunctionProjection(
                "upper",
                NHibernateUtil.String,
                arguments[0].Accept<IProjection>(this)
            );
        }

        public IProjection ToLowerMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            
            return new SqlFunctionProjection(
                "lower",
                NHibernateUtil.String,
                arguments[0].Accept<IProjection>(this)
            );
        }

        public IProjection TrimMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            
            return new SqlFunctionProjection(
                "trim",
                NHibernateUtil.String,
                arguments[0].Accept<IProjection>(this)
            );
        }

        public IProjection IndexOfMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            
            return new SqlFunctionProjection(
                "locate",
                NHibernateUtil.Int32,
                arguments[1].Accept<IProjection>(this),
                arguments[0].Accept<IProjection>(this),
                Projections.Constant(1)
            );
        }

        public IProjection CeilingMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            
            return new SqlFunctionProjection(
                "ceil",
                NHibernateUtil.Int32,
                arguments[0].Accept<IProjection>(this)
            );
        }

        public IProjection FloorMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            
            return new SqlFunctionProjection(
                "floor",
                NHibernateUtil.Int32,
                arguments[0].Accept<IProjection>(this)
            );
        }

        public IProjection RoundMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            
            return new SqlFunctionProjection(
                "round",
                NHibernateUtil.Int32,
                arguments[0].Accept<IProjection>(this)
            );
        }

        public IProjection CastMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            
            var projection = arguments[0].Accept<IProjection>(this);
            if (false == arguments[1] is ConstantNode) {
                return null;
            }

            var typeReference = ((ConstantNode)arguments[1]).TypeReference;
            if (typeReference.IsByte() || 
                typeReference.IsSByte() || 
                typeReference.IsInt16() || 
                typeReference.IsInt32() || 
                typeReference.IsInt64()) {
                return new SqlFunctionProjection("round", NHibernateUtil.Int64, projection);
            }
            return projection;
        }

        public IProjection YearMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            return DatePartMethod(arguments, "year");
        }

        public IProjection MonthMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            return DatePartMethod(arguments, "month");
        }

        public IProjection DayMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            return DatePartMethod(arguments, "day");
        }

        public IProjection HourMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            return DatePartMethod(arguments, "hour");
        }

        public IProjection MinuteMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            return DatePartMethod(arguments, "minute");
        }

        public IProjection SecondMethod(IEnumerable<QueryNode> queryNode) {
            var arguments = queryNode.ToList();
            return DatePartMethod(arguments, "second");
        }

        private IProjection DatePartMethod(IEnumerable<QueryNode> queryNode, string function) {
            var arguments = queryNode.ToList();
            var projection = arguments[0].Accept<IProjection>(this);
            if (false == arguments[1] is ConstantNode) {
                return null;
            }
            return new SqlFunctionProjection(function, NHibernateUtil.Int32, projection);
        }     

        private IType ArtithmicReturnType(QueryNode left, QueryNode right) {
            if (left.Kind == QueryNodeKind.Constant) {
                return nHibernateTypeFromLiteralType(((ConstantNode)left).TypeReference);
            }
            if (right.Kind == QueryNodeKind.Constant) {
                return nHibernateTypeFromLiteralType(((ConstantNode)right).TypeReference);
            }
            return DefaultArithmeticReturnType;
        }

        private IType nHibernateTypeFromLiteralType(IEdmTypeReference type) {
            if (type.IsString()) {
                return NHibernateUtil.String;
            }
            if (type.IsBoolean()) {
                return NHibernateUtil.Boolean;
            }
            if (type.IsSingle()) {
                return NHibernateUtil.Single;
            }
            if (type.IsDouble()) {
                return NHibernateUtil.Double;
            }
            if (type.IsDecimal()) {
                return NHibernateUtil.Decimal;
            }
            if (type.IsInt16()) {
                return NHibernateUtil.Int16;
            }
            if (type.IsInt32()) {
                return NHibernateUtil.Int32;
            }
            if (type.IsInt64()) {
                return NHibernateUtil.Int64;
            }
            if (type.IsBinary()) {
                return NHibernateUtil.Binary;
            }
            if (type.IsDate()) {
                return NHibernateUtil.DateTime;
            }
            if (type.IsGuid()) {
                return NHibernateUtil.Guid;
            }
            if (type.IsDateTimeOffset()) {
                return NHibernateUtil.TimeSpan;
            }
            return DefaultArithmeticReturnType;
        }
    }
}